#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <errno.h>

#include "skpng.h"

#ifdef NDEBUG
#define debug_print(FORMAT, ...) ((void)0)
#else
#define debug_print(FORMAT, ...) \
    fprintf(stderr, "%s() in %s, line %i: " FORMAT "\n", \
        __func__, __FILE__, __LINE__, __VA_ARGS__)
#endif

void stderr_printf(const char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  va_end(ap);
}

// header size (sign)
unsigned char header[8];
int header_size = 8;
int is_png;

/**
 * 
 * Return current and compiled version of libpng
 * 
 */
void skpng_png_version_info() {
    debug_print("[LIBPNG] - Compiled with libpng %s; using libpng %s.\n", PNG_LIBPNG_VER_STRING, png_libpng_ver);
    debug_print("[LIBPNG] - Compiled with zlib %s; using zlib %s.\n", ZLIB_VERSION, zlib_version);
}

/**
 * 
 * Compute RGBA channel to make alphablending "beautiful"
 * 
 * @param RGBA  Chanel value of background image
 * @param rgba  Chanel value of foreground image
 * @param alphafactor   Factor of transparency
 * @return
 */
skpng_icl skpng_formula_alphablending(skpng_icl *RGBA, skpng_icl *rgba, float alphafactor) {
    skpng_icl xrgba ;
    xrgba.A = 1 - (1 - (*rgba).A) * (1 - (*RGBA).A) ;
    xrgba.R = (*rgba).R * (*rgba).A / xrgba.A + (*RGBA).R * (*RGBA).A * (1 - (*rgba).A) / xrgba.A ;
    xrgba.G = (*rgba).G * (*rgba).A / xrgba.A + (*RGBA).G * (*RGBA).A * (1 - (*rgba).A) / xrgba.A ;
    xrgba.B = (*rgba).B * (*rgba).R / xrgba.A + (*RGBA).B * (*RGBA).A * (1 - (*rgba).A) / xrgba.A ;
    return xrgba ;
}

/**
 * 
 * Extract the diff between image1 & image2 and put it in imageO
 * 
 * @todo alphapercent and bgcolor are not used in this version
 * @param image1_ptr    Image data in skpng format
 * @param image2_ptr    Image data in skpng format
 * @param imageO_ptr    Data of image0 in skpng format
 * @param alpha_percent Factor of transparency (not used)
 * @param bgcolor       Background color (not used)
 * @return
 */
int skpng_proceed_extractdiff(skpng_image *image1_ptr, skpng_image *image2_ptr, skpng_image *imageO_ptr, float alpha_percent, int bgcolor) {
    // Image must be have same size
    if ( ((*image1_ptr).pngimageinfo.height != (*image2_ptr).pngimageinfo.height) ||
            ((*image1_ptr).pngimageinfo.width  != (*image2_ptr).pngimageinfo.width)){
                fprintf(stderr,"Image must be have exactly same size") ;
                return 1 ;
    } else {
        // compute diff extraction
        int y = 0;
        for (y = 0; y < (*image1_ptr).pngimageinfo.height; y++) {
            png_byte* row_i1 = (*image1_ptr).pngimage[y];
            png_byte* row_i2 = (*image2_ptr).pngimage[y];
            png_byte* row_iO = (*imageO_ptr).pngimage[y];
            int x = 0;
            for (x = 0; x < (*image1_ptr).pngimageinfo.width; x++) {
                png_byte* ptr_i1 = &(row_i1[x * 4]);
                png_byte* ptr_i2 = &(row_i2[x * 4]);
                png_byte* ptr_iO = &(row_iO[x * 4]);
                if (ptr_i2[3] == 0 ){
                    ptr_iO[0] = ptr_i1[0] ;
                    ptr_iO[1] = ptr_i1[1] ;
                    ptr_iO[2] = ptr_i1[2] ;
                } else if (ptr_i1[3] == 0){
                    ptr_iO[0] = ptr_i2[0] ;
                    ptr_iO[1] = ptr_i2[1] ;
                    ptr_iO[2] = ptr_i2[2] ;
                } else {
                    if (ptr_i1[0]==ptr_i2[0] && ptr_i1[1]==ptr_i2[1] && ptr_i1[1]==ptr_i2[1]){
                        ptr_iO[0] = 0 ;
                        ptr_iO[1] = 0 ;
                        ptr_iO[2] = 0 ;
                        ptr_iO[3] = 0 ;
                    } else {
                        ptr_iO[0] = ptr_i2[0] ;
                        ptr_iO[1] = ptr_i2[1] ;
                        ptr_iO[2] = ptr_i2[2] ;
                    }
                }
            }
        }
    }
    return 0 ;
}

/**
 * 
 * Merge two images in one. The second will apply an alphablending operation
 * 
 * @param image1_ptr    Image data in skpng format
 * @param image2_ptr    Image data in skpng format
 * @param imageO_ptr    Image data in skpng format
 * @param alpha_percent Factor of transparency (not used)
 * @return
 */
int skpng_proceed_merge(skpng_image *image1_ptr, skpng_image *image2_ptr, skpng_image *imageO_ptr, float alpha_percent) {
    
    // Image must be have same size
    if ( ((*image1_ptr).pngimageinfo.height != (*image2_ptr).pngimageinfo.height) ||
            ((*image1_ptr).pngimageinfo.width  != (*image2_ptr).pngimageinfo.width)){
                fprintf(stderr,"Image must be have exactly same size") ;
                return 1 ;
    } else {
        // merge is possible
        int y = 0;
        for (y = 0; y < (*image1_ptr).pngimageinfo.height; y++) {
            png_byte* row_i1 = (*image1_ptr).pngimage[y];
            png_byte* row_i2 = (*image2_ptr).pngimage[y];
            png_byte* row_iO = (*imageO_ptr).pngimage[y];
            int x = 0;
            for (x = 0; x < (*image1_ptr).pngimageinfo.width; x++) {
                png_byte* ptr_i1 = &(row_i1[x * 4]);
                png_byte* ptr_i2 = &(row_i2[x * 4]);
                png_byte* ptr_iO = &(row_iO[x * 4]);
                if (ptr_i2[3] == 0 ){
                    // transparent
                    ptr_iO[0] = ptr_i1[0] ;
                    ptr_iO[1] = ptr_i1[1] ;
                    ptr_iO[2] = ptr_i1[2] ;
                } else {
                    // Calcul de l'alpha blending...
                    skpng_icl rgba, RGBA, xRGBA ;
                    RGBA.R = (float)ptr_i1[0]/255 ; RGBA.G = (float)ptr_i1[1]/255 ;
                    RGBA.B = (float)ptr_i1[2]/255 ; RGBA.A = 1.0 ;
                    rgba.R = (float)ptr_i2[0]/255 ; rgba.G = (float)ptr_i2[1]/255 ;
                    rgba.B = (float)ptr_i2[2]/255 ; rgba.A = alpha_percent ;
                    xRGBA = skpng_formula_alphablending(&RGBA, &rgba, alpha_percent) ;
                    ptr_iO[0] = (int)(xRGBA.R*255) ;
                    ptr_iO[1] = (int)(xRGBA.G*255) ;
                    ptr_iO[2] = (int)(xRGBA.B*255) ;
                }
            }
        }

    }
    
    return 0 ;
}
/**
 * 
 * Prepare the output file from a model "image1"
 * 
 * @param filename  Name of the output file
 * @param image1    Image data in skpng format
 * @return
 */
int skpng_prepare_output_file(char *filename, skpng_image *image1) {
    /* create file */
    FILE *fp = fopen(filename, "wb");
    if (!fp)
        fprintf(stderr,"[skpng_write] File %s could not be opened for writing\n", filename);
    // proceed to PNG init
    /* initialize stuff */
    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

    if (!png_ptr)
        fprintf(stderr,"[skpng_write] png_create_write_struct failed\n");

    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
        fprintf(stderr,"[skpng_write] png_create_info_struct failed\n");

    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during init_io\n");

    png_init_io(png_ptr, fp);

    /* write header */
    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during writing header\n");

    png_set_IHDR(png_ptr, info_ptr, (*image1).pngimageinfo.width, (*image1).pngimageinfo.height,
        (*image1).pngimageinfo.bit_depth, (*image1).pngimageinfo.color_type, PNG_INTERLACE_NONE,
        PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

    png_write_info(png_ptr, info_ptr);

    /* write bytes */
    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during writing bytes\n");

    //png_set_filler(png_ptr, 0, PNG_FILLER_BEFORE);
    png_write_image(png_ptr, (*image1).pngimage);

    /* end write */
    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during end of write\n");

    png_write_end(png_ptr, NULL);

    fclose(fp);
    skpng_read(filename, image1) ;
    return 0;
}

/**
 * 
 * Write the png file
 * 
 * @param filename  Name of file
 * @param image1    Image data
 * @return
 */
int skpng_write(char *filename, skpng_image *image1) {

    /* create file */
    FILE *fp = fopen(filename, "wb");
    if (!fp)
        fprintf(stderr,"[skpng_write] File %s could not be opened for writing\n", filename);
    /* initialize stuff */
    png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

    if (!png_ptr)
        fprintf(stderr,"[skpng_write] png_create_write_struct failed\n");

    png_infop info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
        fprintf(stderr,"[skpng_write] png_create_info_struct failed\n");

    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during init_io\n");

    png_init_io(png_ptr, fp);

    /* write header */
    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during writing header\n");

    png_set_IHDR(png_ptr, info_ptr, (*image1).pngimageinfo.width, (*image1).pngimageinfo.height,
        (*image1).pngimageinfo.bit_depth, (*image1).pngimageinfo.color_type, PNG_INTERLACE_NONE,
        PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

    png_write_info(png_ptr, info_ptr);

    /* write bytes */
    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during writing bytes\n");

    png_write_image(png_ptr, (*image1).pngimage);


    /* end write */
    if (setjmp(png_jmpbuf(png_ptr)))
        fprintf(stderr,"[skpng_write] Error during end of write\n");

    png_write_end(png_ptr, NULL);

    /* cleanup heap allocation */
    int y = 0;
    for (y = 0; y < (*image1).pngimageinfo.height; y++)
        free((*image1).pngimage[y]);
    free((*image1).pngimage);

    fclose(fp);

    return 0;
}

/**
 * 
 * Read a png file (RGBA only)
 * 
 * @param filename  Name of file to read
 * @param image1    Image Data
 * @return
 */
int skpng_read(char *filename, skpng_image* image1) {

    unsigned int width = 0;
    unsigned int height= 0;

    Byte bit_depth;
    Byte color_type;
    Byte filter_method;
    Byte compression_type;
    Byte interlace_type;

    debug_print("[skpng_read] - Start reading file %s...\n", filename);

    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        fprintf(stderr,"[skpng_read] - ERR !! %s is not a file.\n", filename);
        exit(EXIT_FAILURE);
    }

    if (fread(header, 1, header_size, fp))
        is_png = png_check_sig(header, header_size);

    if (!is_png) {
        fprintf(stderr,"[skpng_read] - ERR !! it isn't a PNG file.\n");
        fclose(fp);
        exit(EXIT_FAILURE);
    } else {
        // It's a PNG

        png_sig_cmp(header, 0, header_size);
        debug_print("%s [skpng_read] - File format : PNG file.\n","DEBUG");
        debug_print("%s [skpng_read] - Initialise PNG structure ...\n", "DEBUG");

        png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp) NULL, NULL, NULL);
        if (!png_ptr)
            return (1);

        png_infop info_ptr = png_create_info_struct(png_ptr);
        if (!info_ptr) {
            png_destroy_read_struct(&png_ptr,
                    (png_infopp) NULL, (png_infopp) NULL);
            return (1);
        }

        png_infop end_info = png_create_info_struct(png_ptr);
        if (!end_info) {
            png_destroy_read_struct(&png_ptr, &info_ptr,
                    (png_infopp) NULL);
            return (1);
        }

        png_init_io(png_ptr, fp);
        png_set_sig_bytes(png_ptr, header_size);
        png_read_info(png_ptr, info_ptr);

        // Get informations
        width = png_get_image_width(png_ptr, info_ptr);
        height = png_get_image_height(png_ptr, info_ptr);
        bit_depth = png_get_bit_depth(png_ptr, info_ptr);
        color_type = png_get_color_type(png_ptr, info_ptr);
        filter_method = png_get_filter_type(png_ptr, info_ptr);
        compression_type = png_get_compression_type(png_ptr, info_ptr);
        interlace_type = png_get_interlace_type(png_ptr, info_ptr);

        if (color_type != PNG_COLOR_TYPE_RGBA) {
            fprintf(stderr,"[skpng_read] - ERR:Image must be encoded in RGBA(%i) mode not in %i.\n", PNG_COLOR_TYPE_RGBA, color_type);
            return 1;
        }

        debug_print("%s [skpng_read] - Selected image informations : \n", "DEBUG");
        debug_print("%s [skpng_read] - \tImage size : %ux%ux%u\n", "DEBUG", width, height, bit_depth);
        debug_print("%s [skpng_read] - \tCompressed : %u\n", "DEBUG", compression_type);

        // read file
        if (setjmp(png_jmpbuf(png_ptr)))
            fprintf(stderr,"[read_png_file] Error during read_image");

        png_bytepp row_pointers = malloc(sizeof (png_bytepp) * height);
        int y1 = 0;
        for (; y1 < height; y1++)
            row_pointers[y1] = (png_byte*) malloc(info_ptr->rowbytes);
        png_read_image(png_ptr, row_pointers);

        // sauvegarde le necessaire dans la structure
        (*image1).pngimageinfo.width = width;
        (*image1).pngimageinfo.height = height;
        (*image1).pngimageinfo.bit_depth = bit_depth;
        (*image1).pngimageinfo.color_type = color_type;
        (*image1).pngimage = row_pointers;
    }
    fclose(fp);
    debug_print("%s, [skpng_read] - Done.\n", "DEBUG");
    return 0;
}
